﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Windows;
using System.Xml.Linq;
using NUnit.Framework;
using System.Linq;
using Utility3D.Extensions;

namespace Utility3D
{
    [TestFixture]
    public class ModelObjectXmlGeneratorTest
    {
        [Test]
        public void TestPersistence()
        {
            var worldModel = DefaultWorldModelGenerator.GetModel();
            var drawingSurface = worldModel.DrawingSurfaceChains.First();
            var p1 = worldModel.Collections.AddPoint(drawingSurface, new Point(10, 10));
            var p2 = worldModel.Collections.AddPoint(drawingSurface, new Point(200, 100));
            worldModel.Collections.AddStroke<LineStroke>(p1, p2);

            var xml = worldModel.ToXml().Element("world_model");
            if (xml == null) throw new Exception("invalid xml");

            Assert.AreEqual(3, xml.Elements().Count(element => element.Name.LocalName == "DrawingSurface"));
            Assert.AreEqual(2, xml.Elements().Count(element => element.Name.LocalName == "PointRef"));
            Assert.AreEqual(1, xml.Elements().Count(element => element.Name.LocalName == "LineStroke"));

            AssertElementsHaveAttributes(xml, "DrawingSurface", new[] {"Name", "Height", "Width", "Id", "type"}, new[] {"Plane"});
            AssertElementsHaveAttributes(xml, "Plane", new[] {"type"}, new[] {"BasePoint", "VectorX", "VectorY"});
            AssertElementsHaveAttributes(xml, "BasePoint", new[] {"type", "X", "Y", "Z"}, new string[] {});
            AssertElementsHaveAttributes(xml, "VectorX", new[] {"type", "X", "Y", "Z"}, new string[] {});
            AssertElementsHaveAttributes(xml, "VectorY", new[] {"type", "X", "Y", "Z"}, new string[] {});
            AssertElementsHaveAttributes(xml, "PointRef", new[] {"Id", "type"}, new[] {"keyValuePair"});
            AssertElementsHaveAttributes(xml, "keyValuePair", new[] {"Key"}, new[] {"Value"});
            AssertElementsHaveAttributes(xml, "Value", new[] {"type", "X", "Y"}, new string[] {});
            AssertElementsHaveAttributes(xml, "LineStroke", new[] {"Start", "End", "Id", "type"}, new string[] {});

            worldModel = new WorldModel();
            ModelObjectXmlSerializer.PopulateWorldModel(xml, worldModel);

            var regeneratedXml = worldModel.ToXml();

            var differences = XmlDiff.GetDifferences(xml, regeneratedXml.Element("world_model"));
            Assert.AreEqual(0, differences.Count());
        }

        private static void AssertElementsHaveAttributes(XContainer xml, string elementName, IEnumerable<string> attributes, IEnumerable<string> elementChildren)
        {
            var drawingSurfaceElement = xml.Descendants().First(element => element.Name.LocalName == elementName);
            var missingElements = attributes.Except(drawingSurfaceElement.Attributes().Select(attr => attr.Name.LocalName));
            if (missingElements.Any())
                Assert.Fail("Some attributes were missing on " + elementName + ": " + missingElements.Select(s => "'" + s + "'").SeparateBy(", "));
            missingElements = elementChildren.Except(drawingSurfaceElement.Elements().Select(elem => elem.Name.LocalName));
            if (missingElements.Any())
                Assert.Fail("Some children elements were missing on " + elementName + ": " + missingElements.Select(s => "'" + s + "'").SeparateBy(", "));
        }
    }

    public static class ModelObjectXmlSerializer
    {
        private static IEnumerable<MemberInfo> GetMembersToProcess(object o)
        {
            return o.GetType().GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                .Concat<MemberInfo>(o.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
                .Where(member => member.MemberType == MemberTypes.Field || member.MemberType == MemberTypes.Property)
                .Where( member => member.GetGetter() != null && member.GetSetter() != null && !member.HasAttribute<ExcludeFromPersistenceAttribute>())
                .Where( member => member.HasAttribute<IncludeInPersistenceAttribute>() || (member.MemberType == MemberTypes.Property && member.IsPublic()));
        }

        public static void PopulateWorldModel(XElement element, WorldModel worldModel)
        {
            var newObjects = element
                .Elements()
                .Select(elem => InstantiateObject(worldModel, elem))
                .Cast<IModelObject>()
                .ToDictionary(modelObject => modelObject.Id);

            foreach (var newObject in newObjects.Values)
                PopulateObject(worldModel, element, newObjects, newObject);

            foreach (var o in newObjects.Values)
                worldModel.AddObject(o);
        }

        private static void PopulateObject(WorldModel worldModel, XElement element, Dictionary<int, IModelObject> newObjects, IModelObject modelObject)
        {
            var processedMemberNames = modelObject
                .GetType()
                .GetConstructors()
                .Single()
                .GetParameters()
                .Select(parameter => parameter.Name.ToUpper()[0] + parameter.Name.Substring(1, parameter.Name.Length - 1))
                .ToDictionary(s => s);
            processedMemberNames["Id"] = "Id";

            var membersToProcess = GetMembersToProcess(modelObject)
                .Where(member => !processedMemberNames.ContainsKey(member.Name))
                .Select(member => new
                                      {
                                          Setter = member.GetSetter(),
                                          Value = GetValueForMember(worldModel, newObjects, member.Name, member.ReturnType(), element.Elements().Single(elem => elem.Attribute("Id").Value == modelObject.Id.ToString()))
                                      });

            foreach (var memberToProcess in membersToProcess)
                memberToProcess.Setter(modelObject, memberToProcess.Value);
        }

        private static object InstantiateObject(WorldModel worldModel, XElement modelObjectElement)
        {
            var typeName = modelObjectElement.Attributes("type").First().Value;
            //TODO: Super hack - fix this
            var objectType = typeof (IModelObject).Assembly.GetTypes().Concat(new[] {typeof (Point)}).Single(
                    type => type.Name == typeName);

            var constructors = objectType.GetConstructors();
            if (constructors.Count() > 1) throw new Exception("Can only support 1 constructor currently");
            var constructor = constructors.First();

            var parameters = constructor.GetParameters().Select(parameter => GetValueForMember(worldModel, new Dictionary<int, IModelObject>(),  parameter.Name, parameter.ParameterType, modelObjectElement)).ToArray();
            var newObject = constructor.Invoke(parameters);

            var modelObject = newObject as IModelObject;
            if (modelObject != null)
            {
                var setter = modelObject.GetType().GetSetter<IModelObject, int>(m => m.Id);
                setter(modelObject, Convert.ToInt32(modelObjectElement.Attributes("Id").First().Value));
            }
            return newObject;
        }

        private static object GetValueForMember(WorldModel worldModel, Dictionary<int, IModelObject> newObjects, string memberName, Type memberType, XElement modelObjectElement)
        {
            if (memberType == typeof(WorldModel))
                return worldModel;

            var attributeName = memberName.ToUpper()[0] + memberName.Substring(1, memberName.Length - 1);
            if (memberType.IsSystemType())
            {
                var attribute = modelObjectElement.Attributes().SingleOrDefault(attr => attr.Name.LocalName == attributeName);
                if (attribute == null) return null;

                var attributeValue = modelObjectElement.Attributes().Single(attr => attr.Name.LocalName == attributeName).Value;
                return Convert.ChangeType(attributeValue, memberType);
            }
            if (memberType.IsValueType)
            {
                return InstantiateObject(worldModel, modelObjectElement.Elements().Single(elem => elem.Name.LocalName == attributeName));
            }
            if (typeof(IModelObject).IsAssignableFrom(memberType))
            {
                var attribute = modelObjectElement.Attributes().SingleOrDefault(attr => attr.Name.LocalName == attributeName);
                if (attribute == null) return null;

                return newObjects[Convert.ToInt32(attribute.Value)];
            }
            if (typeof(IDictionary).IsAssignableFrom(memberType))
            {
                var dictionary = Activator.CreateInstance(memberType);
                var arguments = memberType.GetGenericArgumentsForType(typeof (IDictionary<,>)).ToArray();
                var keyValuePairElements = modelObjectElement.Elements().Where(element => element.Name.LocalName == "keyValuePair");
                foreach (var keyValuePairElement in keyValuePairElements)
                {
                    var key = GetValueForMember(worldModel, newObjects, "Key", arguments[0], keyValuePairElement);
                    var value = GetValueForMember(worldModel, newObjects, "Value", arguments[1], keyValuePairElement);
                    var method = memberType.GetMethod("Add");

                    method.Invoke(dictionary, new [] {key, value});
                }
                return dictionary;
            }

            throw new NotImplementedException("Currently only supporting IsValueType and IsSystemType for populating constructors");
        }

        public static XElement GetXml(object o, string name)
        {
            var element = new XElement(name, new XAttribute("type", o.GetType().Name));

            foreach (var member in GetMembersToProcess(o))
            {
                var getter = member.GetGetter();
                var propertyValue = getter(o);
                if (propertyValue == null) continue;

                var memberName = member.Name;

                AddValueToElement(propertyValue, element, memberName);
            }

            return element;
        }

        private static void AddValueToElement(object value, XContainer element, string memberName)
        {
            var propertyType = value.GetType();

            if (typeof(IModelObject).IsAssignableFrom(propertyType))
            {
                var modelProperty = (IModelObject) value;
                element.Add(new XAttribute(memberName, modelProperty.Id));
            }
            else if (propertyType.IsSystemType())
            {
                element.Add(new XAttribute(memberName, value.ToString()));
            }
            else if (propertyType.IsValueType)
            {
                element.Add(GetXml(value, memberName));
            }
            else if (typeof(IDictionary).IsAssignableFrom(propertyType))
            {
                var dictionary = (IDictionary) value;
                foreach (var key in dictionary.Keys)
                {
                    var keyValuePairElement = new XElement("keyValuePair");
                    AddValueToElement(key, keyValuePairElement, "Key");
                    AddValueToElement(dictionary[key], keyValuePairElement, "Value");
                    element.Add(keyValuePairElement);
                }
            }
        }
    }
}